/**
 * (created at 2011-1-26)
 */
package qiushuo.mysqlparser.recognizer.parser;

import static qiushuo.mysqlparser.recognizer.SQLTokenType.IDENTIFIER;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_INSERT;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_KEY;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_ON;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_UPDATE;

import java.sql.SQLSyntaxErrorException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import qiushuo.mysqlparser.ast.expression.Expression;
import qiushuo.mysqlparser.ast.statement.DMLInsertStatement;
import qiushuo.mysqlparser.recognizer.SQLToken;
import qiushuo.mysqlparser.recognizer.TokenReader;
import qiushuo.mysqlparser.util.Pair;

/**
 * @author <a href="mailto:QiuShuo1985@gmail.com">QIU Shuo</a>
 */
public class DMLInsertStmtParser extends DMLInsertReplaceParser {
    /**
     * state of field {@link #input} should never be changed during construction
     */
    public DMLInsertStmtParser(TokenReader input) {
        super(input);
    }

    /**
     * nothing has been pre-consumed <code><pre>
     * 'INSERT' ('LOW_PRIORITY'|'DELAYED'|'HIGH_PRIORITY')? 'IGNORE'? 'INTO'? tbname 
     *  (  'SET' colName ('='|':=') (expr|'DEFAULT') (',' colName ('='|':=') (expr|'DEFAULT'))*
     *   | '(' (  colName (',' colName)* ')' ( ('VALUES'|'VALUE') value (',' value)*
     *                                        | '(' 'SELECT' ... ')'
     *                                        | 'SELECT' ... 
     *                                       )
     *          | 'SELECT' ... ')'
     *         )
     *   |('VALUES'|'VALUE') value  ( ',' value )*
     *   | 'SELECT' ...
     *  )
     * ( 'ON' 'DUPLICATE' 'KEY' 'UPDATE' colName ('='|':=') expr ( ',' colName ('='|':=') expr)* )?
     * 
     * value := '(' (expr|'DEFAULT') ( ',' (expr|'DEFAULT'))* ')'
     * </pre></code>
     */
    public DMLInsertStatement insertStmt() throws SQLSyntaxErrorException {
        match(KW_INSERT);
        int mode = DMLInsertStatement.LDH_UNDEF;
        boolean ignore = false;
        SQLToken token;
        String tableName;
        loop1: while (true) {
            token = input.next();
            if (token == null) throw err("unexpected EOF after INSERT");
            switch (token.getType()) {
            case KW_LOW_PRIORITY:
                if (mode != DMLInsertStatement.LDH_UNDEF) err("duplicated mode for insert: " + token);
                mode = DMLInsertStatement.LDH_LOW;
                continue;
            case KW_DELAYED:
                if (mode != DMLInsertStatement.LDH_UNDEF) err("duplicated mode for insert: " + token);
                mode = DMLInsertStatement.LDH_DELAY;
                continue;
            case KW_HIGH_PRIORITY:
                if (mode != DMLInsertStatement.LDH_UNDEF) err("duplicated mode for insert: " + token);
                mode = DMLInsertStatement.LDH_HIGH;
                continue;
            case KW_IGNORE:
                if (ignore) err("duplicated IGNORE for insert");
                ignore = true;
                continue;
            case KW_INTO:
                token = match(IDENTIFIER);
                tableName = token.getText();
                break loop1;
            case IDENTIFIER:
                tableName = token.getText();
                break loop1;
            default:
                throw err("unexpected token after INSERT: " + token);
            }
        }
        // -------- after table name-----------------------------------------------
        List<String> columnNameList = null;
        List<List<Expression>> valuesList = null;
        Expression select = null;
        boolean onDup = false;
        int alt = -1;
        token = input.next();
        if (token == null) throw err("unexpected EOF after INSERT");
        switch (token.getType()) {
        case KW_SET:
            columnNameList = new LinkedList<String>();
            valuesList = new ArrayList<List<Expression>>(1);
            if (!(onDup = setColumnsFormat(columnNameList, valuesList))) {
                return new DMLInsertStatement(mode, ignore, tableName, columnNameList, valuesList, null);
            }
            alt = ALT_SET;
            break;
        case PUNC_LEFT_PAREN:
            token = input.next();
            if (token == null) throw err("unexpected EOF after INSERT");
            switch (token.getType()) {
            case IDENTIFIER:
                columnNameList = new LinkedList<String>();
                columnNameList.add(token.getText());
                columnList(columnNameList);
                if ((token = input.next()) == null) throw err("unexpected EOF after INSERT");
                switch (token.getType()) {
                case PUNC_LEFT_PAREN:
                case KW_SELECT:
                    input.backForce();
                    select = select();
                    onDup = skipNextOnDuplicateKeyUpdate();
                    alt = ALT_PAREN_COL_SELECT;
                    break;
                case KW_VALUES:
                    valuesList = new LinkedList<List<Expression>>();
                    onDup = valuesList(valuesList);
                    alt = ALT_PAREN_COL_VALUE;
                    break;
                case IDENTIFIER:
                    if ("VALUE".equals(token.getText().toUpperCase())) {
                        valuesList = new LinkedList<List<Expression>>();
                        onDup = valuesList(valuesList);
                        alt = ALT_PAREN_COL_VALUE;
                        break;
                    }
                default:
                    throw err("unexpected token in INSERT: " + token);
                }
                break;
            case KW_SELECT:
                input.skipForce(-2);
                select = select();
                onDup = skipNextOnDuplicateKeyUpdate();
                alt = ALT_SELECT;
                break;
            default:
                throw err("unexpected token after INSERT table '(': " + token);
            }
            break;
        case KW_SELECT:
            input.backForce();
            select = select();
            onDup = skipNextOnDuplicateKeyUpdate();
            alt = ALT_SELECT;
            break;
        case KW_VALUES:
            valuesList = new LinkedList<List<Expression>>();
            onDup = valuesList(valuesList);
            alt = ALT_VALUE;
            break;
        case IDENTIFIER:
            if ("VALUE".equals(token.getText().toUpperCase())) {
                valuesList = new LinkedList<List<Expression>>();
                onDup = valuesList(valuesList);
                alt = ALT_VALUE;
                break;
            }
        default:
            throw err("unexpected token in INSERT: " + token);
        }
        //----------on duplicate key update--------------------------------------------------
        List<Pair<String, Expression>> duplicateUpdate = null;
        if (onDup) {
            duplicateUpdate = afterOnDupKeyUp();
        }
        switch (alt) {
        case ALT_SELECT:
        case ALT_PAREN_COL_SELECT:
            return new DMLInsertStatement(mode, ignore, tableName, columnNameList, select, duplicateUpdate);
        case ALT_PAREN_COL_VALUE:
        case ALT_SET:
        case ALT_VALUE:
            return new DMLInsertStatement(mode, ignore, tableName, columnNameList, valuesList, duplicateUpdate);
        }
        throw err("unrecognized alt for INSERT: " + alt);
    }

    private static final int ALT_PAREN_COL_VALUE = 2;
    private static final int ALT_PAREN_COL_SELECT = 4;
    private static final int ALT_SET = 5;
    private static final int ALT_VALUE = 6;
    private static final int ALT_SELECT = 7;

    private boolean skipNextOnDuplicateKeyUpdate() {
        if (input.isNext(KW_ON, 0)
            && input.isNext(IDENTIFIER, 1, true, "DUPLICATE")
            && input.isNext(KW_KEY, 2)
            && input.isNext(KW_UPDATE, 3)) {
            input.skipForce(4);
            return true;
        }
        return false;
    }

    private boolean valuesList(List<List<Expression>> valuesList) throws SQLSyntaxErrorException {
        return valuesList(valuesList, true);
    }

    private boolean setColumnsFormat(List<String> columnNameList, List<List<Expression>> valuesList)
            throws SQLSyntaxErrorException {
        return setColumnsFormat(columnNameList, valuesList, true);
    }

    /**
     * first <code>UPDATE</code> has been consumed<br/>
     * at least one assignment
     * 
     * @return if <code>ON DUPLICATE KEY UPDATE</code>
     * @throws SQLSyntaxErrorException
     */
    private List<Pair<String, Expression>> afterOnDupKeyUp() throws SQLSyntaxErrorException {
        List<Pair<String, Expression>> list = new LinkedList<Pair<String, Expression>>();
        while (true) {
            SQLToken token = match(IDENTIFIER);
            String colName = token.getText();
            token = input.next();
            if (token == null) err("unexpected EOF in INSERT");
            switch (token.getType()) {
            case OP_ASSIGN:
            case OP_EQUALS:
                break;
            default:
                err("unexpected token: " + token);
            }
            Expression expr = exprParser.expression();
            list.add(new Pair<String, Expression>(colName, expr));
            if ((token = input.next()) == null) {
                return list;
            }
            switch (token.getType()) {
            case PUNC_COMMA:
                continue;
            default:
                input.backForce();
                return list;
            }
        }
    }
}
